/**
 * FIXME:
 * 1. matrix ●>if $a; then echo aaa;else echo bbb; fi   // 此命令错误
 *    matrix ●>if $a ; then echo aaa;else echo bbb; fi  // 此命令正确，原因在于 $var 不能被 ; 打断
 */

#include <stdarg.h>
#include "terminal.h"
#include "general.h"
#include "mmblock.h"
#include "mstring.h"
#include "shstyle.h"
#include "mcurses.h"
#include "thread.h"
#include "device.h"
#include "assert.h"
#include "carray.h"
#include "mxsetup.h"
#include "conductor.h"
#include "ansi.h"
#include "syntax.h"
#include "array.h"

#define CMDLEN MXSETUP_TERMINAL_CMDLEN
#define HISLEN MXSETUP_TERMINAL_HISTORYCOUNT
#define SHELL_STACK_SIZE MXSETUP_TERMINAL_STACKSIZE

#define VAR_NAMELEN MXSETUP_VARIABLE_NAMELEN
#define VAR_VALUELEN MXSETUP_VARIABLE_VALUELEN
#define VAR_COUNT MXSETUP_VARIABLE_COUNT

typedef enum
{
    MX_TERMINAL_STATE_INPUTING = 0, // wating for key input
    MX_TERMINAL_STATE_WRITING,      // printing to the terminal
    MX_TERMINAL_STATE_READING       // called the terminal_read() and wait key input
} mx_terminal_state_t;

typedef struct
{
    mx_uint8_t c;
    mx_carray_node_t node;
} ch_t, *ch_pt;

typedef struct
{
    char cmd[CMDLEN + 1];
    mx_carray_node_t node;
} his_cmd_t, *his_cmd_pt;

typedef struct
{
    char name[VAR_NAMELEN];  // >= 2
    mx_uint8_t value[VAR_VALUELEN]; // >= 4
    mx_carray_node_t node;
} var_t, *var_pt;

static struct
{
    mx_oarray_t cmds;
    mx_device_pt cdev;              // console device
    volatile mx_terminal_state_t state; // the state of terminal for print
    mx_terminal_ops_pt ops;
    struct
    {
        mx_carray_t cs;             // char(s)
        mx_mmblock_t pool;          // free char(s) memory pool
        mx_carray_node_pt cursor;   // the cursor position, if it is `mx_null` means that in first
        ch_t buf[CMDLEN];
    } input;
    struct
    {
        mx_carray_t cmds;
        mx_carray_node_pt pointer;  // current history entry
        mx_mmblock_t pool;          // free history command string pool
        his_cmd_t buf[HISLEN];
    } history;
    struct
    {
        mx_carray_t vars;
        mx_mmblock_t pool;
        var_t buf[VAR_COUNT];
    } variable;
    struct
    {
        mx_bool_t silent;           // mx_true means that don't print on terminal but save the data to buffer
        mx_bool_t lastcwl;          // record whether called mx_terminal_writel or end with `ENDL` last time in shell thread
        mx_usize_t wp;              // buffer write pointer
        char buf[MX_DEFAULT_CONSOLE_BUFSIZE];   // stdout buffer for record terminal output in shell thread
    } stdout;
} terminal;

/* shell thread */
static mx_thread_t shelltd;
static mx_align_cpu mx_uint8_t shellstk[SHELL_STACK_SIZE];

/* cmd string buffer for internal use, it's a temporary */
static mx_uint8_t cmd_strbuf[CMDLEN + 1];

/* function declear */
static void ptable_code_handler (mx_uint8_t ch);

/* ansi csi capture */
static struct
{
#define CSI_BUF_SIZE 16     // csi receive buffer size
#define CSI_CAP_TIMEOUT 18  // csi capture timeout (ms)
    mx_bool_t isesc;        // is esc code, '\e'
    mx_bool_t iscsi;        // is csi code, '['
    mx_uint8_t wpointer;    // the write pointer of buffer
    mx_uint8_t buf[CSI_BUF_SIZE];
} csicap = {
    .isesc = mx_false,
    .iscsi = mx_false,
    .wpointer = 0
};

/**
 * add a command to list
 * @param pcmd: the descriptor of command
 * @param name: command name
 * @param func: the callback funciton of command
 * @return: void
 */
void mx_terminal_cmdadd (mx_terminal_cmd_pt pcmd, mx_string_pt name, mx_string_pt end,
                         mx_terminal_callback_t func)
{
    assert(pcmd == mx_null);
    assert(name == mx_null || name[0] == '\0');
    assert(end != mx_null && end[0] == '\0');
    assert(func == mx_null);
    pcmd->name = name;
    pcmd->end = end;
    pcmd->func = func;
    mx_oarray_node_init(&pcmd->onode, name[0]);
    mx_oarray_add(&terminal.cmds, &pcmd->onode);
}

/**
 * remove a command from list
 * @param pcmd: the descriptor of command
 * @return: void
 */
void mx_terminal_cmdremove (mx_terminal_cmd_pt pcmd)
{
    assert_true(pcmd != mx_null);
    mx_oarray_remove(&pcmd->onode);
}

/**
 * let the console silent ot not
 * @param issilent: is silent or not
 * @note: this function only effect terminal_write(l) which called in shell thread
 * @return: void
 */
void mx_terminal_setsilent (mx_bool_t issilent)
{
    terminal.stdout.silent = issilent;
    /* reset stdout-buffer's write pointer (wp), and `ENDL` mark */
    if (issilent)
    {
        terminal.stdout.wp = 0;
        terminal.stdout.lastcwl = mx_false;
    }
}

/* @note: this function will preappend ' ' if last write with 'newline' end automatically,
          and the `ENDL` which is not in the start or end of the string, we use ' ' replace */
mx_inline static void stdout_buf_append (mx_string_pt str)
{
    /* preappend ' ' automatically */
    if (terminal.stdout.lastcwl)
    {
        if (terminal.stdout.wp < MX_DEFAULT_CONSOLE_BUFSIZE - 1)
        {
            terminal.stdout.buf[terminal.stdout.wp++] = ' ';
        }
        else
        {
            terminal.stdout.buf[MX_DEFAULT_CONSOLE_BUFSIZE - 1] = '\0';
            return;
        }
    }
    /* delete spaces */
    mx_str_foreachi(str, pc, i, {
        if (terminal.stdout.wp < MX_DEFAULT_CONSOLE_BUFSIZE - 1)
        {
            if (str[i] == '\r' && str[i + 1] == '\n')
            {
                if (str[i + 2] != '\0')
                {
                    terminal.stdout.buf[terminal.stdout.wp++] = ' ';
                    i ++;
                }
                else
                {
                    terminal.stdout.buf[terminal.stdout.wp] = '\0'; // terminal.stdout.wp needn't increase
                    /* mark contain `ENDL` last time */
                    terminal.stdout.lastcwl = mx_true;
                    return;
                }
            }
            else if (str[i] == '\n')
            {
                if (str[i + 1] != '\0')
                {
                    terminal.stdout.buf[terminal.stdout.wp++] = ' ';
                }
                else
                {
                    terminal.stdout.buf[terminal.stdout.wp] = '\0'; // terminal.stdout.wp needn't increase
                    /* mark contain `ENDL` last time */
                    terminal.stdout.lastcwl = mx_true;
                    return;
                }
            }
            else
            {
                terminal.stdout.buf[terminal.stdout.wp++] = str[i];
            }
        }
        else
        {
            break;
        }
    });
    terminal.stdout.buf[terminal.stdout.wp] = '\0'; // terminal.stdout.wp needn't increase
    /* mark no `ENDL` in the end */
    terminal.stdout.lastcwl = mx_false;
}

/**
 * write a string on terminal
 * @param fmt: string format
 * @param ...: va_list
 * @return: void
 */
void mx_terminal_write (const mx_string_pt fmt, ...)
{
    va_list args;
    char outbuf[MX_DEFAULT_CONSOLE_BUFSIZE];
    /* generate format string, the (bufsize - 1) is for '\0' */
    va_start(args, fmt);
    mx_vsnprintf(outbuf, MX_DEFAULT_CONSOLE_BUFSIZE, fmt, args);
    va_end(args);
    /* copy data to stdout when current thread is shell */
    if (mx_conductor_active() == &shelltd && terminal.stdout.silent == mx_true)
    {
        stdout_buf_append(outbuf);
    }
    else
    {
        /* do print */
        if (terminal.state != MX_TERMINAL_STATE_WRITING)
        {
            terminal.state = MX_TERMINAL_STATE_WRITING;
            mx_mcurses_newline();
            mx_shstyle_change(MX_SHSTYLE_MODE_NONE);
        }
        console_puts(outbuf);
    }
}

/**
 * write a string on terminal with newline
 * @param fmt: string format
 * @param ...: va_list
 * @return: void
 */
void mx_terminal_writel (const mx_string_pt fmt, ...)
{
    va_list args;
    char outbuf[MX_DEFAULT_CONSOLE_BUFSIZE];    // index(bufsize-1) = '\0', it can be write to '\r', and need append '\0' at last
    /* generate format string, the (bufsize - 1) is for '\0' */
    va_start(args, fmt);
#ifdef CONSOLE_NEWLINE_SPECIAL
    mx_usize_t len = mx_vsnprintf(outbuf, MX_DEFAULT_CONSOLE_BUFSIZE - 2, fmt, args);
#else
    mx_usize_t len = mx_vsnprintf(outbuf, MX_DEFAULT_CONSOLE_BUFSIZE - 1, fmt, args);
#endif
    va_end(args);
    /* add new line */
#ifdef CONSOLE_NEWLINE_SPECIAL
        if (len > MX_DEFAULT_CONSOLE_BUFSIZE - 3)   // outbuf[len] = '\0'
        {
            len = MX_DEFAULT_CONSOLE_BUFSIZE - 3;
        }
        outbuf[len++] = '\r';
        outbuf[len++] = '\n';
        outbuf[len++] = '\0';
#else
        if (len > MX_DEFAULT_CONSOLE_BUFSIZE - 2)   // outbuf[len] = '\0'
        {
            len = MX_DEFAULT_CONSOLE_BUFSIZE - 2;
        }
        outbuf[len++] = '\n';
        outbuf[len++] = '\0';
#endif
    /* copy data to stdout when current thread is shell */
    if (mx_conductor_active() == &shelltd && terminal.stdout.silent == mx_true)
    {
        stdout_buf_append(outbuf);
    }
    else
    {
        if (terminal.state != MX_TERMINAL_STATE_WRITING)
        {
            terminal.state = MX_TERMINAL_STATE_WRITING;
            mx_mcurses_newline();
            mx_shstyle_change(MX_SHSTYLE_MODE_NONE);
        }
        console_puts(outbuf);
    }
}

mx_inline static void input_string_clear ()
{
    /* the node needn't be initialize, because of it will be init when printable char insert */
    mx_carray_init(&terminal.input.cs);
    terminal.input.cursor = mx_null;
    mx_mmblock_freeall(&terminal.input.pool);
}

/* get the input string from `cs` */
mx_inline static mx_string_pt input_string_fetch ()
{
    mx_uint8_t i = 0;
    cmd_strbuf[0] = '\0';
    mx_carray_foreach(&terminal.input.cs, chnode, {
        ch_pt chr = container(chnode, ch_t, node);
        cmd_strbuf[i++] = chr->c;
    });
    cmd_strbuf[i] = '\0';
    return cmd_strbuf;
}

/* get the input string from `cs`, then put it to history buffer
   attention that this function return a string address which is in the history buffer */
mx_inline static mx_string_pt input_string_fetch_record ()
{
    mx_string_pt ret = mx_null;

    /* check whether the input buffer has data */
    if (mx_carray_count(&terminal.input.cs) > 0)
    {
        /* allocate a history buffer */
        his_cmd_pt hiscmd = (his_cmd_pt)mx_mmblock_alloc(&terminal.history.pool);
        if (hiscmd == mx_null)
        {
            mx_carray_node_pt lastnode = mx_carray_lastnode(&terminal.history.cmds);
            mx_carray_remove(lastnode);
            hiscmd = container(lastnode, his_cmd_t, node);
        }
        hiscmd->cmd[0] = '\0';
        /* copy data to history buffer */
        ch_pt chr;
        mx_uint8_t i = 0;
        mx_carray_foreach(&terminal.input.cs, chnode, {
            chr = container(chnode, ch_t, node);
            hiscmd->cmd[i++] = chr->c;
        });
        hiscmd->cmd[i] = '\0';
        mx_carray_preppend(&terminal.history.cmds, &hiscmd->node);
        /* deduplication */
        mx_carray_node_pt nhiscmd_node = mx_carray_node_next(&hiscmd->node);
        if (&hiscmd->node != nhiscmd_node)
        {
            his_cmd_pt nhiscmd = container(nhiscmd_node, his_cmd_t, node);
            if (mx_strcmp(hiscmd->cmd, nhiscmd->cmd) == 0)
            {
                mx_carray_remove(nhiscmd_node);
                mx_mmblock_free(&terminal.history.pool, nhiscmd);
            }
        }
        ret = hiscmd->cmd;
    }
    return ret;
}

/* get all of string after cursor */
mx_inline static mx_string_pt input_string_after_cursor ()
{
    cmd_strbuf[0] = '\0';
    if (mx_carray_count(&terminal.input.cs) > 0)
    {
        mx_carray_node_pt next;
        if (terminal.input.cursor == mx_null)
        {
            next = mx_carray_firstnode(&terminal.input.cs);
        }
        else
        {
            next = mx_carray_node_islast(terminal.input.cursor) ? mx_null : mx_carray_node_next(terminal.input.cursor);
        }
        if (next != mx_null)
        {
            mx_usize_t i;
            for (i = 0; !mx_carray_node_islast(next); i++)
            {
                ch_pt chr = container(next, ch_t, node);
                cmd_strbuf[i] = chr->c;
                next = mx_carray_node_next(next);
            }
            cmd_strbuf[i] = container(next, ch_t, node)->c;
            cmd_strbuf[i + 1] = '\0';
        }
    }
    return cmd_strbuf;
}

mx_inline static void history_view (mx_bool_t isup)
{
    mx_bool_t isaval = mx_false;
    if (mx_carray_count(&terminal.history.cmds) != 0)
    {
        if (terminal.history.pointer == mx_null)
        {
            if (isup)
            {
                terminal.history.pointer = mx_carray_firstnode(&terminal.history.cmds);
                isaval = mx_true;
            }
        }
        else
        {
            if (isup && mx_carray_node_islast(terminal.history.pointer) == mx_false)
            {
                terminal.history.pointer = mx_carray_node_next(terminal.history.pointer);
                isaval = mx_true;
            }
            else if (isup == mx_false && mx_carray_node_isfirst(terminal.history.pointer) == mx_false)
            {
                terminal.history.pointer = mx_carray_node_prev(terminal.history.pointer);
                isaval = mx_true;
            }
        }
        /* check whether a history entry has been selected */
        if (isaval)
        {
            his_cmd_pt cmd = container(terminal.history.pointer, his_cmd_t, node);
            /* clear the input buffer of terminal */
            mx_mcurses_clearline();
            mx_shstyle_drawhead();
            /* clear input string and put new string */
            input_string_clear();
            for (mx_size_t i = 0; i < mx_strlen(cmd->cmd); i++)
            {
                ptable_code_handler((mx_uint8_t)cmd->cmd[i]);
            }
        }
    }
}

mx_inline static void history_reset ()
{
    terminal.history.pointer = mx_null;
}

mx_inline static mx_bool_t cursor_moveleft ()
{
    mx_bool_t ret = mx_false;
    if (terminal.input.cursor != mx_null)
    {
        if (mx_carray_node_isfirst(terminal.input.cursor))
        {
            terminal.input.cursor = mx_null;
        }
        else
        {
            terminal.input.cursor = mx_carray_node_prev(terminal.input.cursor);
        }
        ret = mx_true;
    }
    return ret;
}

mx_inline static mx_bool_t cursor_moveright ()
{
    mx_bool_t ret = mx_false;
    if (terminal.input.cursor == mx_null)
    {
        if (mx_carray_count(&terminal.input.cs) == 0)
        {
            ret = mx_false;
        }
        else
        {
            terminal.input.cursor = mx_carray_firstnode(&terminal.input.cs);
            ret = mx_true;
        }
    }
    else
    {
        if (!mx_carray_node_islast(terminal.input.cursor))
        {
            terminal.input.cursor = mx_carray_node_next(terminal.input.cursor);
            ret = mx_true;
        }
    }
    return ret;
}

/* 0 S 1 S 2 S 3 */
mx_inline static mx_size_t cursor_index ()
{
    mx_size_t ret = 0;
    if (terminal.input.cursor != mx_null)
    {
        ret = mx_carray_index(terminal.input.cursor) + 1;
    }
    return ret;
}

mx_inline static mx_bool_t cursor_backspace ()
{
    mx_bool_t ret = mx_false;
    if (terminal.input.cursor != mx_null)
    {
        mx_carray_node_pt tmp = terminal.input.cursor;
        terminal.input.cursor = mx_carray_node_isfirst(terminal.input.cursor) ? mx_null : mx_carray_node_prev(terminal.input.cursor);
        mx_carray_remove(tmp);
        ret = mx_true;
    }
    return ret;
}

// mx_inline static void input_reset ()
// {
//     mx_carray_init(&terminal.input.cs);
//     terminal.input.cursor = mx_null;
//     mx_mmblock_freeall(&terminal.input.pool);
// }

/* resume the interrupted state if need */
mx_inline static void input_mode_resume ()
{
    if (terminal.state !=  MX_TERMINAL_STATE_INPUTING)
    {
        /* update state */
        terminal.state = MX_TERMINAL_STATE_INPUTING;
        /* resume */
        mx_mcurses_newline();
        mx_shstyle_drawhead();
        mx_string_pt istr = input_string_fetch();
        if (!mx_strisempt(istr))
        {
            mx_shstyle_change(MX_SHSTYLE_MODE_CONTENT);
            console_puts(istr);
            /* recover the position of cursor */
            mx_mcurses_move(MX_MCURSES_DIRECTION_LEFT,
                            mx_carray_count(&terminal.input.cs) - cursor_index());
        }
    }
}

static mx_bool_t var_syntax_check (mx_string_pt str)
{
    mx_bool_t ret = mx_false;
    if (ANSI_ISALPHA(str[0]) || str[0] == '_')   // first [A-Za-z_]
    {
        ret = mx_true;
        mx_str_foreach(str, pc, {
            if (!ANSI_ISALPHA(*pc) && *pc != '_' && !ANSI_ISNUMBER(*pc))    // others [A-Za-z0-9_]*
            {
                ret = mx_false;
                break;
            }
        });
    }
    return ret;
}

static mx_uint8_pt var_search (mx_string_pt name)
{
    mx_uint8_pt ret = mx_null;
    mx_carray_foreach(&terminal.variable.vars, varnode, {
        var_pt var = container(varnode, var_t, node);
        if (mx_strcmp(var->name, name) == 0)
        {
            ret = var->value;
            break;
        }
    });
    return ret;
}

/* for syntax "" '' */
static mx_string_pt syntax_default (mx_syntax_cell_pt pcell, mx_string_pt ctx, mx_string_pt resbuf, mx_usize_t bufsize)
{
    return ctx;
}

/* for syntax ${} */
static mx_string_pt syntax_var (mx_syntax_cell_pt pcell, mx_string_pt ctx, mx_string_pt resbuf, mx_usize_t bufsize)
{
    mx_string_pt value = mx_null;
    if (var_syntax_check(ctx))
    {
        value = var_search(ctx);
        /* return a empty string when the variable doesn't exist */
        if (value == mx_null)
        {
            resbuf[0] = '\0';
            value = resbuf;
        }
    }
    return value;
}

/* for syntax $() */
static mx_string_pt syntax_cmd (mx_syntax_cell_pt pcell, mx_string_pt ctx, mx_string_pt resbuf, mx_usize_t bufsize)
{
    mx_string_pt ret;
    /* close the output on terminal but put the data to std-buffer */
    mx_terminal_setsilent(mx_true);
    if (mx_terminal_run(ctx) == MX_TERMINAL_EXECRET_OK)
    {
        /* return the std-buffer if execute command successful */
        ret = terminal.stdout.buf;
    }
    else
    {
        /* return a empty string when the execute command failed */
        resbuf[0] = '\0';
        ret = resbuf;
    }
    mx_terminal_setsilent(mx_false);
    return ret;
}

/* for syntax $? $# */
static mx_string_pt syntax_varfixed (mx_syntax_cell_pt pcell, mx_string_pt ctx, mx_string_pt resbuf, mx_usize_t bufsize)
{
    return var_search(pcell->stop);
}

/* syntax cells */
static mx_syntax_cell_t cells[] = {
    { .start = "\"", .stop = "\"", .type = MX_SYNTAX_TYPE_SYMMETRIC, .sbreak = mx_false, .snest = mx_true, .callback = syntax_default },
    { .start = "'", .stop = "'", .type = MX_SYNTAX_TYPE_SYMMETRIC, .sbreak = mx_false, .snest = mx_false, .callback = syntax_default },
    { .start = "${", .stop = "}", .type = MX_SYNTAX_TYPE_SEQUENTIAL, .sbreak = mx_false, .snest = mx_false, .callback = syntax_var },
    { .start = "$(", .stop = ")", .type = MX_SYNTAX_TYPE_SEQUENTIAL, .sbreak = mx_false, .snest = mx_true, .callback = syntax_cmd },
    { .start = "$", .stop = "?", .type = MX_SYNTAX_TYPE_SEQUENTIAL, .sbreak = mx_false, .snest = mx_false, .callback = syntax_varfixed },
    { .start = "$", .stop = " ", .type = MX_SYNTAX_TYPE_SEQUENTIAL, .sbreak = mx_true, .snest = mx_false, .callback = syntax_var },
};

static mx_terminal_cmd_pt command_search (mx_string_pt str)
{
    mx_terminal_cmd_pt ret = mx_null;
    mx_oarray_foreach(&terminal.cmds, onode, {
        mx_terminal_cmd_pt cmd = container(onode, mx_terminal_cmd_t, onode);
        if (mx_strcmp(cmd->name, str) == 0)
        {
            ret = cmd;
            break;
        }
    });
    return ret;
}

/* call the function of command, the input `cmdstr` will be modify,
   the `cmdstr` can't be mx_null. if `command` == null then do search */
static mx_size_t do_command_exe (mx_string_pt cmdstr, mx_terminal_cmd_pt command)
{
    mx_size_t ret = MX_TERMINAL_EXECRET_CMDNOTFOUND;
    if (!mx_strisempt(cmdstr))
    {
        /* split the arguments */
        mx_size_t argc = mx_strsplitc_trim(cmdstr, ' ', mx_null);
        mx_string_pt argv[argc];
        mx_strsplitc_trim(cmdstr, ' ', argv);
        /* match the command */
        if (command == mx_null)
        {
            mx_terminal_cmd_pt cmd = command_search(argv[0]);
            if (cmd != mx_null)
                ret = cmd->func(argc, argv);
        }
        else
            ret = command->func(argc, argv);
    }
    else
        ret = MX_TERMINAL_EXECRET_ERR;
    return ret;
}

/**
 * run a command, multiple commands use `;` to separate
 * @param str: the command string, it can't be a const variable
 * @note: one of command return MX_TERMINAL_EXECRET_BREAK will interrupt this function
 * @return: the result of command
 */
mx_size_t mx_terminal_run (mx_string_pt str)
{
    mx_size_t ret = MX_TERMINAL_EXECRET_ERR;
    if (str != mx_null && !mx_strisempt(str))
    {
        /* it is used as a buffer inside mx_syntax_cell_parser */
        char nextbuf[CMDLEN + 1];
        /* discard last ';', last ';' is support */
        mx_strtrim(str);    // trim the end spaces
        if (mx_strendwithc(str, ';'))
            str[mx_strlen(str) - 1] = '\0';
        /* copy string to a temporery */
        char tempstr[CMDLEN + 1];
        mx_strcpy(tempstr, str);
        /* split command by ';' */
        mx_usize_t cnt = mx_strsplitc(str, ';', mx_null);
        mx_string_pt arr[cnt];
        mx_strsplitc(str, ';', arr);
        /* split temp command by ';', and every element will be trim() */
        mx_string_pt arr_trimed[cnt];
        mx_strsplitc(tempstr, ';', arr_trimed);
        /* syntax checkout, `;;` is error */
        mx_array_foreachi(arr_trimed, s, i, {
            arr_trimed[i] = mx_strtrim(*s);
            if (mx_strisempt(arr_trimed[i]))
                return MX_TERMINAL_EXECRET_ERR_SYNTAX;
        });
        mx_size_t idx;
        /* analysis commands and execute it */
        mx_array_foreachi(arr_trimed, s, i, {
            /* find the first word */
            if ((idx = mx_strci(*s, ' ')) != -1)
                (*s)[idx] = '\0';
            /* match command */
            mx_terminal_cmd_pt cmd = command_search(*s);
            /* recover '\0' to ' ' */
            if (idx != -1)
                (*s)[idx] = ' ';
            /* matched a command */
            if (cmd != mx_null)
            {
                /* checkout the terminator of command, we should recover type `if xx;xx; fi` commandm
                   because the `tempstr` splited by ';' */
                if (cmd->end != mx_null)
                {
                    char stmp[CMDLEN + 1];
                    /* find the index of terminator and skip whole command in outter loop,
                       support nesting, we need special treatment for command that end with `done`, bacause the left symbol is `do` */
                    mx_size_t nestcnt = 0;
                    if (mx_strcmp(cmd->end, "done") == 0)
                    {
                        for (; i < cnt; i++)
                        {
                            if (mx_strcmp(arr_trimed[i], cmd->end) == 0 && (--nestcnt == 0))
                                break;
                            else    // NOTE: this computation is very performance consuming
                            {
                                /* copy string */
                                mx_strcpy(stmp, arr_trimed[i]);
                                /* split string by ' ' */
                                mx_size_t scnt = mx_strsplitc_trim(stmp, ' ', mx_null);
                                mx_string_pt stmp_arr[scnt];
                                mx_strsplitc_trim(stmp, ' ', stmp_arr);
                                /* search command */
                                mx_array_foreach(stmp_arr, is, {
                                    mx_terminal_cmd_pt icmd = command_search(*is);
                                    if (icmd != mx_null && icmd->end != mx_null && mx_strcmp(icmd->end, cmd->end) == 0)
                                        nestcnt ++;
                                });
                            }
                        }
                    }
                    else
                    {
                        for (; i < cnt; i++)
                        {
                            if (mx_strcmp(arr_trimed[i], cmd->end) == 0 && (--nestcnt == 0))
                                break;
                            else
                            {
                                /* copy string */
                                mx_strcpy(stmp, arr_trimed[i]);
                                /* split string by ' ' */
                                mx_size_t scnt = mx_strsplitc(stmp, ' ', mx_null);
                                mx_string_pt stmp_arr[scnt];
                                mx_strsplitc(stmp, ' ', stmp_arr);
                                /* count the number of cmd->name */
                                mx_array_foreach(stmp_arr, st, {
                                    if (mx_strcmp(*st, cmd->name) == 0)
                                        nestcnt ++;
                                });
                            }
                        }
                    }
                    if (i != cnt)   // find terminator
                    {
                        /* recover command through change '\0' to ';',
                           we can't use arr_trimed (tempstr) because ' ' has been change to '\0', and the recover operation mabey generate `;;` */
                        mx_string_pt start = *s - tempstr + arr[0];     // calculates the corresponding index of `str`
                        for (mx_string_pt pc = start; pc < arr[i]; pc++)
                        {
                            if (*pc == '\0')
                                *pc = ';';
                        }
                        ret = do_command_exe(start, cmd);
                    }
                    else
                        ret = MX_TERMINAL_EXECRET_ERR_SYNTAX;   // NOTE: or MX_TERMINAL_EXECRET_ERR_USAGE ?
                }
                else    // the command type is `echo $?`, we should do syntax analysis first
                {
                    /* do syntax analysis, it will anylize all of the ${} $() "" '' and so on,
                       the cmd in the $() will be execute (will call the mx_terminal_run function internally) */
                    mx_string_pt res = mx_syntax_cell_parser(cells, len(cells), *s, nextbuf, CMDLEN + 1);
                    if (res == mx_null)
                        return MX_TERMINAL_EXECRET_ERR_SYNTAX;
                    ret = do_command_exe(res, cmd);
                }
            }
            else    // not a command, mabey var=value or $var (var='echo ehllo')
            {
                mx_string_pt start = *s;    // the command string source can be either `str` or `tempstr`
                /* check whether `$(` exists and recover whole `$(xxx)` command,
                   support nesting */
                if ((idx = mx_strsi(*s, "$(")) != -1)
                {
                    mx_size_t nestcnt = 1;
                    idx += 2;
                    for (; i < cnt; i++, idx = 0)   // skip whole command `$(xxx)` in outter loop
                    {
                        mx_str_foreach(&arr_trimed[i][idx], pc, {
                            if (*pc == ')' && (--nestcnt == 0))
                                break;
                            else if (*pc == '$' && *(pc + 1) == '(')
                                nestcnt ++;
                        });
                        if (nestcnt == 0)
                            break;
                    }
                    if (i != cnt)   // found `)`
                    {
                        /* recover command through change '\0' to ';',
                           we can't use arr_trimed (tempstr) because ' ' has been change to '\0', and the recover operation mabey generate `;;` */
                        start = *s - tempstr + arr[0];      // calculates the corresponding index of `str`
                        for (mx_string_pt pc = start; pc < arr[i]; pc++)
                        {
                            if (*pc == '\0')
                                *pc = ';';
                        }
                    }
                    else
                        return MX_TERMINAL_EXECRET_ERR_SYNTAX;   // NOTE: or MX_TERMINAL_EXECRET_ERR_USAGE ?
                }
                /* shell syntax (=): var=value */
                if ((idx = mx_strci(start, '=')) != -1 &&
                    idx > 0 && idx < (mx_strlen(start) - 1) && start[idx - 1] != ' ' && start[idx + 1] != ' ')
                {
                    start[idx] = '\0';    // set `=` to `\0`
                    /* check whether the left of `=` is shell variable format or not*/
                    if (var_syntax_check(start))
                    {
                        /* analysis right section of `=` */
                        mx_string_pt res = mx_syntax_cell_parser(cells, len(cells), &start[idx + 1], nextbuf, CMDLEN + 1);
                        if (res == mx_null)
                            return MX_TERMINAL_EXECRET_ERR_SYNTAX;
                        /* search variable */
                        mx_uint8_pt value = var_search(start);
                        if (value == mx_null)
                        {
                            /* alloc a new memory for variable */
                            var_pt var = mx_mmblock_alloc(&terminal.variable.pool);
                            if (var != mx_null)
                            {
                                mx_strncpy(var->name, start, VAR_NAMELEN - 1);
                                value = var->value;
                                mx_carray_append(&terminal.variable.vars, &var->node);
                            }
                            else
                                ret = MX_TERMINAL_EXECRET_NOVAR;
                        }
                        /* copy the right section of = to the variable's value */
                        mx_strncpy(value, res, VAR_VALUELEN - 1);
                        ret = MX_TERMINAL_EXECRET_OK;
                    }
                    else
                        ret = MX_TERMINAL_EXECRET_ERR_SYNTAX;
                }
                /* else if () {}    // other case, e.g. && */
                else    // $var (var='echo string')
                {
                    /* do syntax analysis, it will anylize all of the ${} $() "" '' and so on */
                    mx_string_pt res = mx_syntax_cell_parser(cells, len(cells), start, nextbuf, CMDLEN + 1);
                    if (res == mx_null)
                        return MX_TERMINAL_EXECRET_ERR_SYNTAX;
                    ret = do_command_exe(res, mx_null);
                }
            }
            /* check whether need to break */
            if (ret == MX_TERMINAL_EXECRET_BREAK)
                break;
        });
    }
    return ret;
}

/**
 * run a command, and not modify the input string, multiple commands use `;` to separate
 * @param str: the command string
 * @note: This function runs slower than mx_terminal_run()
 * @return: the result of command
 */
mx_size_t mx_terminal_runnm (mx_string_pt str)
{
    mx_size_t ret = MX_TERMINAL_EXECRET_CMDNOTFOUND;
    if (str != mx_null)
    {
        /* copy string */
        char tempstr[CMDLEN + 1];
        mx_strcpy(tempstr, str);
        /* execute command */
        ret = mx_terminal_run(tempstr);
    }
    return ret;
}

static mx_string_pt i2s (mx_int32_t num)
{
    mx_sprintf(cmd_strbuf, CMDLEN + 1, "%d", num);
    return cmd_strbuf;
}

/* only deal with the error about shell, not the command like MX_TERMINAL_EXECRET_ERR_USAGE */
static void error_dispose (mx_size_t code)
{
    switch (code)
    {
    case MX_TERMINAL_EXECRET_ERR_SYNTAX:
        mx_shstyle_change(MX_SHSTYLE_MODE_WARN);
        console_puts("syntax error" ENDL);
        break;
    case MX_TERMINAL_EXECRET_CMDNOTFOUND:
        mx_shstyle_change(MX_SHSTYLE_MODE_WARN);
        console_puts("undefined command" ENDL);
        break;
    }
}

/* printable code handler */
static void ptable_code_handler (mx_uint8_t ch)
{
    /* get a store block for input char from the free memory pool */
    ch_pt chnode = mx_mmblock_alloc(&terminal.input.pool);
    if (chnode != mx_null)
    {
        input_mode_resume();
        /* print on terminal */
        mx_shstyle_change(MX_SHSTYLE_MODE_CONTENT);
        /* point to first and not input anything yet */
        if ((terminal.input.cursor == mx_null && mx_carray_count(&terminal.input.cs) == 0) ||   // point to first and no input yet
            (terminal.input.cursor != mx_null && mx_carray_node_islast(terminal.input.cursor))) // point to last
        {
            console_putc(ch);
        }
        else
        {
            mx_mcurses_insertc(ch, input_string_after_cursor());
        }
        /* write to buffer */
        mx_carray_node_init(&chnode->node);
        chnode->c = ch;
        if (terminal.input.cursor == mx_null)
        {
            /* insert to first */
            mx_carray_preppend(&terminal.input.cs, &chnode->node);
        }
        else
        {
            /* insert to next of cursor */
            mx_carray_node_insert_after(terminal.input.cursor, &chnode->node);
        }
        /* update cursor */
        terminal.input.cursor = &chnode->node;
    }
}

/* contorl code handler */
static void ctrl_code_handler (mx_uint8_t ch)
{
    mx_size_t res;
    mx_string_pt cmd;
    switch (ch)
    {
    case _ANSI_C0_ETX:   // '^C, ctrl c'
        terminal.state = MX_TERMINAL_STATE_INPUTING;
        mx_mcurses_newline();
        history_reset();
        input_string_clear();
        mx_shstyle_drawhead();
        break;
    case _ANSI_C0_BS:    // '\b, backspace'
    case _ANSI_C0_DEL:
        input_mode_resume();
        if (cursor_backspace())
        {
            mx_mcurses_delete(MX_MCURSES_DIRECTION_LEFT, 1);
        }
        break;
    case _ANSI_C0_CR:    // '\r, new line'
        mx_mcurses_newline();
        history_reset();
        /* get input string from oarray and clear it, and then put it to history buffer,
           the mx_strsplitc_trim will change the src-string so copy from history_buffer to cmd_strbuf */
        cmd = input_string_fetch_record();
        if (cmd != mx_null)
        {
            terminal.state = MX_TERMINAL_STATE_WRITING; // called mx_mcurses_newline above, so need mark this
            input_string_clear();
            mx_shstyle_change(MX_SHSTYLE_MODE_NONE);
            /* analysis command and execute */
            mx_strcpy(cmd_strbuf, cmd); // mx_terminal_run maybe modify inpute string, so need copy it from history-buffer to `cmd_strbuf`
            res = mx_terminal_run(cmd_strbuf);
            /* record result to $? */
            mx_strcpy(container(mx_carray_firstnode(&terminal.variable.vars), var_t, node)->value,
                    i2s(res));
        }
        else
            res = MX_TERMINAL_EXECRET_OK;
        /* handle exception */
        error_dispose(res);
        mx_shstyle_drawhead();
        terminal.state = MX_TERMINAL_STATE_INPUTING;
        break;
    case _ANSI_C0_ESC:   // '^E, esc'
        break;
    default:
        break;
    }
}

/* csi code handler */
static void csi_code_handler (mx_uint8_pt pch, mx_uint8_t len)
{
    // NOTE: not check the format of CSI code
    switch (pch[len - 1])
    {
    case 'A':  // cursor up (↑)
        history_view(mx_true);
        break;
    case 'B':  // cursor down (↓)
        history_view(mx_false);
        break;
    case 'C':  // cursor forward (→)
        input_mode_resume();
        if (cursor_moveright())
        {
            mx_mcurses_move(MX_MCURSES_DIRECTION_RIGHT, 1);
        }
        break;
    case 'D':  // cursor back (←)
        input_mode_resume();
        if (cursor_moveleft())
        {
            mx_mcurses_move(MX_MCURSES_DIRECTION_LEFT, 1);
        }
        break;
    }
}

static mx_terminal_ops_t ops = {
    .ptable_code_handler = ptable_code_handler,
    .ctrl_code_handler = ctrl_code_handler,
    .csi_code_handler = csi_code_handler
};

mx_inline static mx_bool_t ansi_csi_code_capture (mx_uint8_t ch)
{
    mx_bool_t ret = mx_false;
    if (ch == ANSI_C0_CSI[0] && csicap.isesc == mx_false)
    {
        csicap.buf[0] = ANSI_C0_CSI[0];
        csicap.isesc = mx_true;
        mx_device_ctrl(terminal.cdev, 1, (mx_void_pt)MX_TIMER_MS(CSI_CAP_TIMEOUT)); // uart read wait for 10ms
        if (mx_device_read(terminal.cdev, &ch, 1) == 1 && ch == ANSI_C0_CSI[1])
        {
            csicap.buf[1] = ANSI_C0_CSI[1];
            csicap.wpointer = 2;
            /* collect function and arguments, the charactor can only be number or ';' or alphabet
               and the alpahbet means that CSI code is over */
            while (mx_device_read(terminal.cdev, &ch, 1) == 1 && csicap.wpointer < CSI_BUF_SIZE)
            {
                csicap.buf[csicap.wpointer++] = ch;
                if (ANSI_ISALPHA(ch))
                {
                    csicap.iscsi = mx_true;
                    ret = mx_true;
                    break;
                }
                if (!ANSI_ISNUMBER(ch) && ch != ';')
                {
                    csicap.iscsi = mx_false;
                    break;
                }
            }
            if (csicap.iscsi)
            {
                terminal.ops->csi_code_handler(csicap.buf, csicap.wpointer);
                csicap.wpointer = 0;
            }
        }
    }
    /* reset flags */
    csicap.isesc = mx_false;
    csicap.iscsi = mx_false;
    return ret;
}

/**
 * read input from terminal
 * @param tick: the tick of timeout, 0 = forever
 * @return: -1 = no byte, otherwise regard it as mx_uint8_t (char)
 */
mx_int16_t mx_terminal_read (mx_usize_t tick)
{
    mx_int16_t ch;
    mx_device_ctrl(terminal.cdev, 1, (mx_void_pt)tick);
    if (mx_device_read(terminal.cdev, &ch, 1) == 0)
    {
        ch = -1;
    }
    return ch;
}

/* the entry point of shell thread */
static mx_int32_t shell_thread_entry (mx_void_pt param)
{
    /* data recevie */
    mx_uint8_t ch;
    mx_uint8_t csiidx;
    /* get the console device */
    terminal.cdev = console_device();
    assert(terminal.cdev == mx_null);

    mx_shstyle_drawhead();
    /* read char */
    while (mx_true)
    {
        /* control the uart wait forever */
        mx_device_ctrl(terminal.cdev, 1, 0);
        /* read a char */
        if (mx_device_read(terminal.cdev, &ch, 1) != 0)
        {
            if (!ansi_csi_code_capture(ch))
            {
                csiidx = 0;
                do
                {
                    /* git the char from csi buffer if it is not empty */
                    if (csiidx < csicap.wpointer)
                    {
                        ch = csicap.buf[csiidx++];
                    }
                    if (ANSI_ISCHAR(ch))         // printable code
                    {
                        terminal.ops->ptable_code_handler(ch);
                    }
                    else if (ANSI_ISCTRL0(ch))   // control code
                    {
                        terminal.ops->ctrl_code_handler(ch);
                    }
                } while (csiidx < csicap.wpointer);
                /* clear the write pointer of csi buffer */
                csicap.wpointer = 0;
            }
        }
    }
}

static void terminal_init ()
{
    terminal.state = MX_TERMINAL_STATE_INPUTING;
    terminal.cdev = mx_null;
    terminal.ops = &ops;
    mx_oarray_init(&terminal.cmds, MX_OARRAY_SORT_ASCE);
    /* input */
    mx_carray_init(&terminal.input.cs);
    mx_mmblock_init(&terminal.input.pool, terminal.input.buf, sizeof(terminal.input.buf), sizeof(ch_t));
    terminal.input.cursor = mx_null;
    /* history */
    terminal.history.pointer = mx_null;
    mx_carray_init(&terminal.history.cmds);
    mx_mmblock_init(&terminal.history.pool, terminal.history.buf, sizeof(terminal.history.buf), sizeof(his_cmd_t));
    /* variable */
    mx_carray_init(&terminal.variable.vars);
    mx_mmblock_init(&terminal.variable.pool, terminal.variable.buf, sizeof(terminal.variable.buf), sizeof(var_t));
    var_pt var = mx_mmblock_alloc(&terminal.variable.pool);
    assert(var == mx_null);
    var->name[0] = '?'; var->name[1] = '\0';    // first node is $?
    var->value[0] = '0'; var->value[1] = '\0';
    mx_carray_preppend(&terminal.variable.vars, &var->node);
    /* commands auto register */
    symbol_foreach(MX_TERMINAL_SYMBOL_CLUSTER, sym, {
        mx_terminal_cmd_pt cmd = (mx_terminal_cmd_pt)sym;
        mx_terminal_cmdadd(cmd, cmd->name, cmd->end, cmd->func);
    });
    /* stdout */
    terminal.stdout.silent = mx_false;
    terminal.stdout.lastcwl = mx_false;
    terminal.stdout.wp = 0;
    /* shell thread initialization */
    mx_thread_mount(&shelltd, "shell", MX_DEFAULT_PRIORITY - 1, MX_DEFAULT_TICK, shellstk,
                    SHELL_STACK_SIZE, shell_thread_entry, mx_null);
    mx_thread_start(&shelltd);
}
MX_BOARDINIT_EXPORT(terminal_init, 2);
